package cn.suimg.app.util;

import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.handler.inter.IExcelDictHandler;
import cn.suimg.app.entity.SysDictItem;
import cn.suimg.app.sso.mapper.SysDictItemMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.data.redis.core.RedisTemplate;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@Slf4j
public class ExcelUtil {

    /**
     * 导入数据字典缓存
     */
    private static final String EXCEL_IMPORT_DICT_CACHE = "excel_import_dict_cache:%s:%s";

    /**
     * 导入数据表缓存
     */
    private static final String EXCEL_IMPORT_TABLE_CACHE = "excel_import_table_cache:%s:%s";

    /**
     * 使用提供的基于字典映射的处理器将数据导出到Excel。
     * dataList 的泛型类型必须与 clazz 的泛型类型相匹配。
     *
     * @param response HttpServletResponse 用于写入Excel文件的响应对象。
     * @param handler  IExcelDictHandler 实例，用于处理基于字典的映射。
     * @param clazz    用于映射 dataList 条目的类类型。
     * @param dataList 要导出的数据列表，其元素类型需与 clazz 匹配。
     * @param title    Excel 文档的标题。
     */
    @SneakyThrows
    public static <T> void export(HttpServletResponse response, IExcelDictHandler handler, Class<T> clazz, List<T> dataList, String title) {
        ExportParams exportParams = new ExportParams(title, null);
        // 自定义字典查询规则
        exportParams.setDictHandler(handler);
        try (Workbook workbook = ExcelExportUtil.exportExcel(exportParams, clazz, dataList)) {
            if (workbook != null) {
                try {
                    response.setCharacterEncoding("UTF-8");
                    response.setHeader("content-Type", "application/vnd.ms-excel");
                    response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(title, "UTF-8") + ".xlsx");
                    workbook.write(response.getOutputStream());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 从Excel输入流中导入数据，并应用字段转换和过滤。
     *
     * <p>此方法使用泛型来支持不同类型的对象导入，并允许通过字段转换器和过滤器来自定义处理逻辑。
     * 它通过反射机制访问对象的字段，并应用指定的转换逻辑。
     *
     * @param <T>         要导入的对象类型。
     * @param inputStream Excel文件的输入流。
     * @param clazz       要导入对象的类类型。
     * @param offset      标题行数，用于跳过Excel文件中的标题行。
     * @return 导入并转换后的对象列表。
     * @see ImportParams
     * @see ExcelImportUtil
     * @see java.util.function.Predicate
     * @see java.util.Map
     * @see java.util.function.Function
     */
    public static <T> List<T> import0(InputStream inputStream, Class<T> clazz, Integer offset) {
        return import0(inputStream, clazz, offset, null);
    }

    /**
     * 从Excel输入流中导入数据，并应用字段转换和过滤。
     *
     * <p>此方法使用泛型来支持不同类型的对象导入，并允许通过字段转换器和过滤器来自定义处理逻辑。
     * 它通过反射机制访问对象的字段，并应用指定的转换逻辑。
     *
     * @param <T>         要导入的对象类型。
     * @param inputStream Excel文件的输入流。
     * @param clazz       要导入对象的类类型。
     * @param offset      标题行数，用于跳过Excel文件中的标题行。
     * @param filter      一个谓词，用于过滤导入的数据。
     * @return 导入并转换后的对象列表。
     * @see ImportParams
     * @see ExcelImportUtil
     * @see java.util.function.Predicate
     * @see java.util.Map
     * @see java.util.function.Function
     */
    public static <T> List<T> import0(InputStream inputStream, Class<T> clazz, Integer offset, Predicate<T> filter) {
        return import0(inputStream, clazz, offset, filter, null);
    }

    /**
     * 从Excel输入流中导入数据，并应用字段转换和过滤。
     *
     * <p>此方法使用泛型来支持不同类型的对象导入，并允许通过字段转换器和过滤器来自定义处理逻辑。
     * 它通过反射机制访问对象的字段，并应用指定的转换逻辑。
     *
     * @param <T>            要导入的对象类型。
     * @param inputStream    Excel文件的输入流。
     * @param clazz          要导入对象的类类型。
     * @param offset         标题行数，用于跳过Excel文件中的标题行。
     * @param filter         一个谓词，用于过滤导入的数据。
     * @param fieldConverter 一个字段到转换器函数的映射，用于字段的值转换。
     * @return 导入并转换后的对象列表。
     * @see ImportParams
     * @see ExcelImportUtil
     * @see java.util.function.Predicate
     * @see java.util.Map
     * @see java.util.function.Function
     */
    @SneakyThrows
    public static <T> List<T> import0(InputStream inputStream, Class<T> clazz, Integer offset, Predicate<T> filter, Map<String, Function<String, String>> fieldConverter) {
        ImportParams params = new ImportParams();
        params.setTitleRows(offset);
        List<T> expressInfoList = ExcelImportUtil.importExcel(inputStream, clazz, params);
        return expressInfoList.stream()
                .filter(filter != null ? filter : (e) -> true)
                .peek(item -> {
                    if (fieldConverter != null) {
                        try {
                            for (Field field : clazz.getDeclaredFields()) {
                                String fieldName = field.getName();
                                Function<String, String> converter = fieldConverter.get(fieldName);
                                if (converter != null) {
                                    field.setAccessible(true);
                                    Object getValue = field.get(item);
                                    String convertValue = converter.apply(String.valueOf(getValue));
                                    field.set(item, convertValue);
                                }
                            }
                        } catch (Exception e) {
                            log.error("invoke field error", e);
                        }
                    }
                }).collect(Collectors.toList());
    }


    /**
     * 构建用于从Redis缓存获取字典类型数据的函数。
     * 如果缓存中不存在数据，则从数据库中查询并缓存。
     *
     * @param dictType 字典类型标识。
     * @return 一个函数，用于获取字典项的值。
     * @throws RuntimeException 如果无法从Spring上下文中获取必要的bean或数据库查询失败。
     */
    public static Function<String, String> dictCacheBuilder(String dictType) {
        RedisTemplate<String, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
        SysDictItemMapper sysDictItemMapper = SpringContextHolder.getBean(SysDictItemMapper.class);
        return value -> {
            String cacheKey = String.format(EXCEL_IMPORT_DICT_CACHE, dictType, value);
            Object result;
            if ((result = redisTemplate.opsForValue().get(cacheKey)) == null) {
                SysDictItem sysDictItem = sysDictItemMapper.selectOne(Wrappers.<SysDictItem>lambdaQuery().eq(SysDictItem::getType, dictType).eq(SysDictItem::getLabel, value));
                result = sysDictItem.getValue();
                redisTemplate.opsForValue().set(cacheKey, result, 10, TimeUnit.MINUTES);
            }
            return String.valueOf(result);
        };
    }

    /**
     * 构建一个用于从Redis缓存获取特定类型的表数据的函数。
     * 如果缓存中不存在数据，则从数据库中根据给定的条件查询并缓存结果。
     *
     * @param <T>               表数据的类型。
     * @param clazz             表数据类的Class对象，表示要查询的数据类型。
     * @param baseMapper        表数据类的Mapper接口，用于执行数据库操作。
     * @param conditionFunction 一个函数，用于从类型T的对象中提取查询条件。
     * @param resultFunction    一个函数，用于从查询结果中提取所需的结果。
     * @return 一个函数，用于根据给定的值获取缓存或数据库中的相应数据。
     * @throws RuntimeException 如果无法从Spring上下文中获取redisTemplate。
     */
    public static <T> Function<String, String> tableCacheBuilder(Class<T> clazz, BaseMapper<T> baseMapper, SFunction<T, Object> conditionFunction, SFunction<T, Object> resultFunction) {
        RedisTemplate<String, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
        String className = clazz.getName();
        return value -> {
            String cacheKey = String.format(EXCEL_IMPORT_TABLE_CACHE, className, value);
            Object result;
            if ((result = redisTemplate.opsForValue().get(cacheKey)) == null) {
                T t = baseMapper.selectOne(Wrappers.<T>lambdaQuery().eq(conditionFunction, value));
                result = resultFunction.apply(t);
                redisTemplate.opsForValue().set(cacheKey, result, 10, TimeUnit.MINUTES);
            }
            return String.valueOf(result);
        };
    }
}
